登录注册页的最后一部分是第三方登录入口和表单组件的封装。第三方登录通过图标列表展示不同的登录渠道(微信、QQ、微博、GitHub 等),用户点击图标后跳转到对应的授权页面。LoginForm 组件的封装则将表单逻辑从页面中抽离,通过 props 和 emits 实现数据通信,为后续的表单校验和复用奠定基础。
第三方登录实现
第三方登录区域使用 el-divider 分隔线与主表单区域隔开,下方通过循环渲染 loginItems 数组中的图标列表:
<!-- components/form/LoginForm.vue -->
<template>
<el-form>
<!-- 主表单内容... -->
<!-- 第三方登录分隔线 -->
<el-divider>
<span class="text-gray-500 text-sm">其他登录方式</span>
</el-divider>
<!-- 第三方登录图标列表 -->
<div class="flex justify-around mt-2.5">
<Iconify
v-for="(item, index) in loginItems"
:key="index"
:icon="item.icon"
class="text-gray-400 text-2xl cursor-pointer
hover:text-[var(--el-color-primary)]"
@click="handleClickItem(item)"
/>
</div>
</el-form>
</template>
vue
图标与样式细节
- 图标大小:通过
text-2xl控制图标尺寸 - 默认颜色:
text-gray-400提供柔和的默认灰色 - 悬停效果:使用
hover:text-[var(--el-color-primary)]让鼠标悬停时变为 Element Plus 的主题色。这里的var(--el-color-primary)是 Element Plus 提供的 CSS 变量,确保图标颜色与系统主题一致
loginItems 数据结构
第三方登录的图标列表由父组件传入,每个 item 包含图标名称和跳转 URL:
// pages/login/index.vue
const loginItems = reactive([
{ icon: 'ri:wechat-fill', url: '/auth/wechat' },
{ icon: 'ri:qq-fill', url: '/auth/qq' },
{ icon: 'ri:weibo-fill', url: '/auth/weibo' },
{ icon: 'ri:github-fill', url: '/auth/github' },
])
typescript
点击图标时触发跳转:
// components/form/LoginForm.vue
const handleClickItem = (item: LoginItem) => {
emit('clickIcon', item)
// 或直接跳转
// window.location.href = item.url
}
typescript
LoginForm 组件封装
将登录表单封装为独立的 LoginForm 组件,通过 props 接收配置,通过 emits 向外传递事件。
Props 设计
// components/form/LoginForm.vue
interface Props {
title?: string // 可选标题
loginItems?: LoginItem[] // 第三方登录列表(可选)
}
const props = withDefaults(defineProps<Props>(), {
title: undefined,
loginItems: undefined,
})
typescript
两个 prop 都是可选的,组件可以不传 title 和 loginItems 正常使用。
Emits 设计
const emit = defineEmits<{
submit: [form: LoginFormInterface]
clickIcon: [item: LoginItem]
}>()
typescript
submit:表单提交时触发,携带表单数据clickIcon:点击第三方登录图标时触发
完整组件代码
<!-- components/form/LoginForm.vue -->
<template>
<div class="login-form-wrapper">
<el-form
ref="formRef"
:model="form"
label-width="0"
>
<!-- 标题 -->
<h2 v-if="title" class="text-xl font-bold mb-6 text-center">
{{ title }}
</h2>
<!-- 用户名 -->
<el-form-item prop="username">
<el-input
v-model="form.username"
placeholder="请输入用户名"
prefix-icon="User"
/>
</el-form-item>
<!-- 密码 -->
<el-form-item prop="password">
<el-input
v-model="form.password"
type="password"
placeholder="请输入密码"
prefix-icon="Lock"
show-password
/>
</el-form-item>
<!-- 再次输入密码(注册场景) -->
<el-form-item prop="rePassword">
<el-input
v-model="form.rePassword"
type="password"
placeholder="请再次输入密码"
prefix-icon="Lock"
show-password
/>
</el-form-item>
<!-- 记住账号 -->
<el-form-item>
<div class="flex justify-between items-center w-full">
<el-checkbox v-model="form.remember">记住账号</el-checkbox>
<el-link type="primary" :underline="false">忘记密码?</el-link>
</div>
</el-form-item>
<!-- 登录按钮 -->
<el-form-item>
<el-button type="primary" class="w-full" @click="handleSubmit">
登录
</el-button>
</el-form-item>
<!-- 第三方登录 -->
<template v-if="loginItems?.length">
<el-divider>
<span class="text-gray-500 text-sm">其他登录方式</span>
</el-divider>
<div class="flex justify-around mt-2.5">
<Iconify
v-for="(item, index) in loginItems"
:key="index"
:icon="item.icon"
class="text-gray-400 text-2xl cursor-pointer
hover:text-[var(--el-color-primary)]"
@click="handleClickItem(item)"
/>
</div>
</template>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import type { LoginItem, LoginFormInterface } from '../../types/login'
interface Props {
title?: string
loginItems?: LoginItem[]
}
const props = withDefaults(defineProps<Props>(), {
title: undefined,
loginItems: undefined,
})
const emit = defineEmits<{
submit: [form: LoginFormInterface]
clickIcon: [item: LoginItem]
}>()
const form = reactive<LoginFormInterface>({
username: '',
password: '',
rePassword: '',
phone: '',
code: '',
remember: false,
})
const handleSubmit = () => {
emit('submit', { ...form })
}
const handleClickItem = (item: LoginItem) => {
emit('clickIcon', item)
}
</script>
vue
类型定义
将接口类型集中管理在 types 文件中:
// types/login.ts
export interface LoginFormInterface {
username: string
password: string
rePassword: string
phone: string | number
code: string
remember: boolean
}
export interface LoginItem {
icon: string
url: string
}
typescript
注意 LoginFormInterface 是表单内部使用的完整接口(包含所有字段),而 LoginFormPrompt 是 props 接口(只包含需要从外部配置的字段)。
在登录页面中使用
<!-- pages/login/index.vue -->
<template>
<LoginForm
:title="title"
:login-items="loginItems"
@submit="handleSubmit"
@click-icon="handleClickIcon"
/>
</template>
<script setup lang="ts">
import LoginForm from '~/components/form/LoginForm.vue'
import type { LoginItem, LoginFormInterface } from '~/types/login'
const title = ref('登录注册页')
const loginItems = reactive<LoginItem[]>([
{ icon: 'ri:wechat-fill', url: '/auth/wechat' },
{ icon: 'ri:github-fill', url: '/auth/github' },
{ icon: 'ri:weibo-fill', url: '/auth/weibo' },
])
const handleSubmit = (form: LoginFormInterface) => {
console.log('用户提交表单:', form)
}
const handleClickIcon = (item: LoginItem) => {
console.log('点击第三方登录:', item)
window.location.href = item.url
}
</script>
vue
总结
LoginForm 组件的封装将表单逻辑(数据管理、提交事件)和 UI 展示(输入框、按钮、第三方图标)集中在一个组件中,通过 props(title、loginItems)和 emits(submit、clickIcon)与外部通信。第三方登录的图标颜色使用 Element Plus 的 CSS 变量 --el-color-primary 确保与系统主题色一致。这种封装模式使得 LoginForm 可以在登录、注册等不同场景下复用,只需传入不同的 props 配置即可。
↑